// @ts-check
import * as THREE from 'three'
import * as CANNON from 'cannon'
import Player from './Player'

class Controls {
  constructor({
    lockTriggerEl,
    cannonBody,
    onPointerLockChange
  }) {
    this._canJump = false
    this._jumpVelocity = 20
    this._movingForward = false
    this._movingBackward = false
    this._movingLeft = false
    this._movingRight = false
    this._velocityFactor = 0.2
    this._inputVelocity = new THREE.Vector3()
    this._euler = new THREE.Euler()
    this._quat = new THREE.Quaternion()
    this._onPointerLockChange = onPointerLockChange
    this._isPointerLocked = false
    this._lockTriggerEl = lockTriggerEl
    this._cannonBody = cannonBody

    this.player = new Player({skeletonVisiable: true}, (mesh) => {
      this.playerLoaded = true
      this.playerMesh = mesh
    })
    // camera container
    // const geometry = new THREE.CubeGeometry(1, 1, 1)
    // const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    // const yawObject = new THREE.Mesh(geometry, material)

    const yawObject = new THREE.Object3D()
    this._yawObject = yawObject
    const pitchObject = new THREE.Object3D()
    this._pitchObject = pitchObject
    const contactNormal = new CANNON.Vec3(); // Normal in the contact, pointing *out* of whatever the player touched

    yawObject.position.y = 10
    yawObject.add(pitchObject)
    this._bindEvents()

    const upAxis = new CANNON.Vec3(0, 1, 0)

    cannonBody.addEventListener('collide', e => {
      const contact = e.contact

      // contact.bi and contact.bj are the colliding bodies, and contact.ni is the collision normal.
      // We do not yet know which one is which! Let's check.
      if (contact.bi.id === cannonBody.id) {  // bi is the player body, flip the contact normal
        contact.ni.negate(contactNormal)
      } else {
        contactNormal.copy(contact.ni) // bi is something else. Keep the normal as it is
      }
      // If contactNormal.dot(upAxis) is between 0 and 1, we know that the contact normal is somewhat in the up direction.
      if (contactNormal.dot(upAxis) > 0.5) { // Use a "good" threshold value between 0 and 1 here!
        this._canJump = true
      }
    })
  }

  onSwitchCamera = cb => {
    this._onSwitchCamera = cb
  }

  onShoot = cb => {
    this._onShoot = cb
  }

  get object() {
    return this._yawObject
  }

  get viewYDeg() {
    return this._yawObject.rotation.y
  }

  get viewXDeg() {
    return this._pitchObject.rotation.x
  }

  get position() {
    return this._cannonBody.position
  }

  get rotation() {
    return {
      y: this.viewYDeg,
      x: this.viewXDeg
    }
  }

  _handlePointerLockChange = () => {
    this._isPointerLocked = !this._isPointerLocked
    this._onPointerLockChange(this._isPointerLocked)
  }

  _bindEvents = () => {
    document.addEventListener('pointerlockchange', this._handlePointerLockChange)
    window.addEventListener('keydown', this._handleKeyDown)
    window.addEventListener('keyup', this._handleKeyUp)
    window.addEventListener('mousemove', this._handleMouseMove)
    window.addEventListener('click', this._handleClick)
    this._lockTriggerEl.addEventListener('click', this.lockPointer)
  }

  addCamera = (camera) => {
    this.camera = camera
    this._pitchObject.add(camera)
  }

  lockPointer = () => {
    if (this._isPointerLocked) {
      return
    }
    document.body.requestPointerLock()
  }

  rotateView = (event) => {
    const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0
    const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0

    this._yawObject.rotation.y -= movementX * 0.002
    this._pitchObject.rotation.x -= movementY * 0.002
  }

  _handleClick = e => {
    if (this._isPointerLocked) {
      this._onShoot()
    }
  }

  _handleMouseMove = e => {
    if (this._isPointerLocked) {
      this.rotateView(e)
    }
  }

  _handleKeyDown = evt => {
    switch (evt.key) {
      case 'w':
        this._movingForward = true
        break
      case 'd':
        this._movingRight = true
        break
      case 's':
        this._movingBackward = true
        break
      case 'a':
        this._movingLeft = true
        break
      case ' ':
        if (this._canJump === true) {
          this._cannonBody.velocity.y = this._jumpVelocity
        }
        this._canJump = false
        break
      case 'v':
        this._onSwitchCamera()
        break
      default:
        break
    }
  }

  _handleKeyUp = evt => {
    switch (evt.key) {
      case 'w':
        this._movingForward = false
        break
      case 'd':
        this._movingRight = false
        break
      case 's':
        this._movingBackward = false
        break
      case 'a':
        this._movingLeft = false
        break
      default:
        break;
    }
  }

  next = delta => {
    const euler = this._euler
    const velocity = this._cannonBody.velocity
    const inputVelocity = this._inputVelocity
    const quat = this._quat
    delta *= 0.1

    inputVelocity.set(0, 0, 0);

    if (this._movingForward) {
      inputVelocity.z = - this._velocityFactor * delta;
      this.player.exec()
    }
    if (this._movingBackward) {
      inputVelocity.z = this._velocityFactor * delta;
    }

    if (this._movingLeft) {
      inputVelocity.x = - this._velocityFactor * delta;
    }
    if (this._movingRight) {
      inputVelocity.x = this._velocityFactor * delta;
    }

    // Convert velocity to world coordinates
    euler.x = this._pitchObject.rotation.x
    euler.y = this._yawObject.rotation.y
    euler.order = 'XYZ'
    quat.setFromEuler(euler)
    inputVelocity.applyQuaternion(quat)

    // Add to the object
    velocity.x += inputVelocity.x
    velocity.z += inputVelocity.z

    this._yawObject.position.copy(this._cannonBody.position)
  }
}

export default Controls
